/** * UI.java * * Copyright (c) 1995-2010, The University of Sheffield. See the file * COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt * * This file is part of GATE (see http://gate.ac.uk/), and is free * software, licenced under the GNU Library General Public License, * Version 2, June 1991 (in the distribution as file licence.html, * and also available at http://gate.ac.uk/gate/licence.html). * * Valentin Tablan, 23 Nov 2011 */ package gate.mimir.web.client; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.http.client.URL; import com.google.gwt.user.client.History; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.ServiceDefTarget; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.InlineHTML; import com.google.gwt.user.client.ui.InlineHyperlink; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.MultiWordSuggestOracle.MultiWordSuggestion; import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.SuggestOracle; import com.google.gwt.user.client.ui.TextArea; import com.google.gwt.user.client.ui.Widget; /** * Entry point classes define <code>onModuleLoad()</code>. */ public class UI implements EntryPoint { private static final Logger logger = Logger.getLogger(UI.class.getName()); /** * A Timer implementation that fetches the latest results information from the * server and updates the results display accordingly. * It will re-schedule itself until all required data is made available by * the server. */ protected class ResultsUpdater extends Timer { private int newFirstDocument; /** * Flag used to indicate if we previously encountered an error * communicating with the remote endpoint. This flag gets cleared on every * successful access. */ boolean remoteError = false; /** * Creates a new results updater. * @param newFirstDocument the first document to be displayed on the page. * This can be used for navigating between pages. */ public ResultsUpdater(int newFirstDocument) { super(); this.newFirstDocument = newFirstDocument; } public void setFirstDocument(int newFirstDocument) { this.newFirstDocument = newFirstDocument; } @Override public void run() { if(newFirstDocument != firstDocumentOnPage) { // new page: clear data and old display firstDocumentOnPage = newFirstDocument; feedbackLabel.setText("Working..."); updateResultsDisplay(null); } gwtRpcService.getResultsData(queryId, firstDocumentOnPage, maxDocumentsOnPage, new AsyncCallback<ResultsData>() { @Override public void onSuccess(ResultsData result) { remoteError = false; updatePage(result); if(result.getResultsTotal() < 0) schedule(500); } @Override public void onFailure(Throwable caught) { if(remoteError) { // this is the second failure: bail out // re-throw the exception so it's seen (useful when debugging) String message = caught.getLocalizedMessage(); if(message == null || message.length() == 0) { message = "Error connecting to index."; } feedbackLabel.setText(message); updateResultsDisplay(null); throw new RuntimeException(caught); } else { // update the flag so we get out next time remoteError = true; } if(caught instanceof MimirSearchException) { if(((MimirSearchException)caught).getErrorCode() == MimirSearchException.QUERY_ID_NOT_KNOWN) { // query ID not known -> re-post the query if(queryString != null && queryString.length() > 0){ queryId = null; postQuery(queryString); return; } } else if(((MimirSearchException)caught).getErrorCode() == MimirSearchException.INTERNAL_SERVER_ERROR) { // server side error: try reposting the query once // clean up old state if(queryId != null) { // release old query gwtRpcService.releaseQuery(queryId, new AsyncCallback<Void>() { @Override public void onSuccess(Void result) {} @Override public void onFailure(Throwable caught) {} }); } if(queryString != null && queryString.length() > 0){ queryId = null; postQuery(queryString); return; } } } // ignore and try again later schedule(500); // re-throw the exception so it's seen (useful when debugging) throw new RuntimeException(caught); } }); } } private class MimirOracle extends SuggestOracle{ public MimirOracle() { super(); gwtRpcService.getAnnotationsConfig(getIndexId(), new AsyncCallback<String[][]>() { public void onFailure(Throwable caught) { //we could not get the data from the server annotationsConfig = new String[][]{ new String[]{} }; } public void onSuccess(String[][] result) { annotationsConfig = result; } }); } /* (non-Javadoc) * @see com.google.gwt.user.client.ui.SuggestOracle#requestSuggestions(com.google.gwt.user.client.ui.SuggestOracle.Request, com.google.gwt.user.client.ui.SuggestOracle.Callback) */ @Override public void requestSuggestions(Request request, Callback callback) { ArrayList<MultiWordSuggestion> suggestions = new ArrayList<MultiWordSuggestion>(); String query = request.getQuery(); int caretIndex = searchBox.getValueBox().getCursorPos(); int startIndex = query.lastIndexOf('{', caretIndex - 1); int endIndex = query.indexOf('{', caretIndex); if (endIndex == -1) { endIndex = caretIndex; } int lastClose = query.lastIndexOf("}", caretIndex); if (startIndex != -1 && lastClose < startIndex) { // an open bracket '{' is present, and not followed by } yet //check if we have the annotation type already String annType = null; boolean nonSpaceSeen = false; int charIdx = startIndex + 1; for(; charIdx < endIndex; charIdx++){ // this method is deprecated, but the replacement (isWhitespace()) // is not implemented in GWT if(Character.isSpace(query.charAt(charIdx))){ if(nonSpaceSeen){ //we found some space, after some actual content was seen annType = query.substring(startIndex + 1, charIdx); break; } }else{ nonSpaceSeen = true; } } if(annType == null){ //we have not found an ann type -> suggest some //the string before the last open {, before the caret String before = query.substring(0, startIndex); //the string after the next { String after = query.substring(endIndex); //the string from the current open {, to the caret, or the next { String middle = (startIndex >= 0 && startIndex < endIndex) ? query.substring(startIndex+1, endIndex): ""; for(int annTypeId = 0; annTypeId < annotationsConfig.length; annTypeId++){ if(annotationsConfig[annTypeId][0].startsWith(middle)){ //we have identified the annotation type String suggestion = "{" + annotationsConfig[annTypeId][0]; suggestions.add(new MultiWordSuggestion( before + suggestion + after, suggestion)); // Window.alert("Suggestion is: \"" + before + suggestion + after + "\"!"); } } }else{ //we know the ann type -> consume everything until the last word int lastSpace = charIdx; int wordCount = 0; boolean inSpace = true; boolean inQuote = false; for(; charIdx < endIndex; charIdx++){ if(inQuote){ //while in quote, consume everything until the closing quote if(query.charAt(charIdx) == '"' && charIdx > 0 && query.charAt(charIdx -1) != '\\'){ inQuote = false; // wordCount++; } }else{ if(Character.isSpace(query.charAt(charIdx))){ lastSpace = charIdx; if(!inSpace){ //we're starting a new space (so we just finished a word) wordCount++; inSpace = true; } }else if(query.charAt(charIdx) == ')'){ //closing of REGEX wordCount = 0; inSpace = false; } else if(query.charAt(charIdx) == '"' && charIdx > 0 && query.charAt(charIdx -1 ) != '\\'){ inQuote = true; inSpace = false; } else{ //some other non-space char if(inSpace){ //we're starting a new word inSpace = false; } } } } if(inQuote){ //suggest nothing }else{ String before = query.substring(0, lastSpace + 1); String after = query.substring(endIndex); String middle = lastSpace < endIndex ? query.substring(lastSpace + 1, endIndex) : ""; //we are still typing the feature name or operator //words appear in this sequence: <feature> <operator> <value> if(wordCount % 3 == 0){ //feature //find the ann type for(int annTypeId = 0; annTypeId < annotationsConfig.length; annTypeId++){ if(annotationsConfig[annTypeId][0].equalsIgnoreCase(annType)){ //suggest some feature names for(int featId = 1; featId < annotationsConfig[annTypeId].length; featId++){ if(annotationsConfig[annTypeId][featId].startsWith(middle)){ String suggestion = annotationsConfig[annTypeId][featId]; suggestions.add(new MultiWordSuggestion( before + suggestion + after, suggestion)); } } //also offer to close the annotation String suggestion = "}"; suggestions.add(new MultiWordSuggestion( before + suggestion + after, suggestion)); //only one ann type can match break; } } } else if(wordCount % 3 == 1){ //operator String[] strArray = new String[]{"= \"\"", "<", "<=", ">", ">=", ".REGEX()"}; for(String suggestion : strArray){ suggestions.add(new MultiWordSuggestion( before + suggestion + after, suggestion)); } }else{ //value -> no suggestions } } } //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // //the string before the last open {, before the caret // String before = query.substring(0, startIndex); // //the string after the next { // String after = query.substring(endIndex); // //the string from the current open {, to the caret, or the next { // String middle = (startIndex >= 0 && startIndex < endIndex) ? // query.substring(startIndex+1, endIndex): ""; // //find the annotation type // String[] inputs = middle.split("\\s"); // if(inputs.length == 1){ // //we're searching only for annotation type // for(int annTypeId = 0; annTypeId < indexConfig.length; annTypeId++){ // if(indexConfig[annTypeId][0].startsWith(inputs[0])){ // //we have identified the annotation type // String suggestion = "{" + indexConfig[annTypeId][0]; // suggestions.add(new MultiWordSuggestion( // before + suggestion + after, suggestion)); // } // } // } else if(inputs.length > 1){ // //we already have the ann type, we need to suggest feature names // for(int annTypeId = 0; annTypeId < indexConfig.length; annTypeId++){ // if(indexConfig[annTypeId][0].equalsIgnoreCase(inputs[0])){ // //now we need to suggest a feature name // //the before string must contain everything up to the current feature // // String lastPrefix = inputs[inputs.length -1]; // if(lastPrefix.indexOf('=') <0){ // //we are still typing the feature name // for(int featId = 1; featId < indexConfig[annTypeId].length; // featId++){ // if(indexConfig[annTypeId][featId].startsWith(lastPrefix)){ // String suggestion = indexConfig[annTypeId][featId] + " = "; // suggestions.add(new MultiWordSuggestion( // before + suggestion + after, suggestion)); // } // } // } // //we only match one annotation type, so we break now // break; // } // } // }//if(inputs.length > 1) } Response response = new Response(suggestions); callback.onSuggestionsReady(request, response); } private String[][] annotationsConfig = new String[][]{new String[]{}}; } /** * Gets the Javascript variable value from the GSP view. * @return */ private native String getIndexId() /*-{ return $wnd.indexId; }-*/; /** * Gets the Javascript variable value from the GSP view. * @return */ private native String getUriIsLink() /*-{ return $wnd.uriIsLink; }-*/; /** * The remote service used to communicate with the server. */ private GwtRpcServiceAsync gwtRpcService; /** * The TextArea where the query string is typed by the user. */ protected SuggestBox searchBox; /** * The Search button. */ protected Button searchButton; /** * The current query ID (used when communicating with the server). */ protected String queryId; /** * The current query string (used to re-post the query if the session expired * (e.g. the link was bookmarked). */ protected String queryString; /** * Cached value for the current index ID (obtained once from * {@link #getIndexId()}, then cached). */ protected String indexId; /** * Cached value for the Javascript var (obtained once from * {@link #getUriIsLink()}, then cached). */ protected boolean uriIsLink; /** * The label displaying feedback to the user (e.g. how many documents were * found, or the current error message). */ protected Label feedbackLabel; /** * The panel covering the centre of the page, where the results documents are * listed. */ protected HTMLPanel searchResultsPanel; /** * The panel at the bottom of the page, containing links to other result * pages. */ protected HTMLPanel pageLinksPanel; protected ResultsUpdater resultsUpdater; /** * The rank of the first document on page. */ protected int firstDocumentOnPage; /** * How many documents should be shown on each result page. */ protected int maxDocumentsOnPage = 20; /** * How many page links should be included at the bottom. The current page * would normally appear in the middle. */ protected int maxPages = 20; /** * How many characters are displayed for each snippet (for longer snippets, * the middle content is truncated and replaced by an ellipsis). */ protected int maxSnippetLength = 150; /** * This is the entry point method. */ public void onModuleLoad() { // connect to the server RPC endpoint gwtRpcService = (GwtRpcServiceAsync) GWT.create(GwtRpcService.class); ServiceDefTarget endpoint = (ServiceDefTarget) gwtRpcService; String rpcUrl = GWT.getHostPageBaseURL() + "gwtRpc"; endpoint.setServiceEntryPoint(rpcUrl); indexId = getIndexId(); uriIsLink = Boolean.parseBoolean(getUriIsLink()); resultsUpdater = new ResultsUpdater(0); initLocalData(); initGui(); initListeners(); } protected void initLocalData() { queryId = null; firstDocumentOnPage = 0; } protected void initGui() { HTMLPanel searchDiv = HTMLPanel.wrap(Document.get().getElementById("searchBox")); TextArea searchTextArea = new TextArea(); searchTextArea.setCharacterWidth(60); searchTextArea.setVisibleLines(10); searchBox = new SuggestBox(new MimirOracle(), searchTextArea); searchBox.setTitle("Press Escape to hide suggestions list; " + "press Ctrl+Space to show it again."); searchBox.addStyleName("mimirSearchBox"); searchDiv.add(searchBox); searchButton = new Button(); searchButton.setText("Search"); searchButton.addStyleName("searchButton"); searchDiv.add(searchButton); HTMLPanel resultsBar = HTMLPanel.wrap(Document.get().getElementById("feedbackBar")); feedbackLabel = new InlineLabel(); resultsBar.add(feedbackLabel); resultsBar.add(new InlineHTML(" ")); searchResultsPanel = HTMLPanel.wrap(Document.get().getElementById("searchResults")); updateResultsDisplay(null); pageLinksPanel = HTMLPanel.wrap(Document.get().getElementById("pageLinks")); pageLinksPanel.add(new InlineHTML(" ")); } protected void initListeners() { searchBox.addKeyUpHandler(new KeyUpHandler() { @Override public void onKeyUp(KeyUpEvent event) { int keyCode = event.getNativeKeyCode(); if(keyCode == KeyCodes.KEY_ENTER && event.isControlKeyDown()){ // CTRL-ENTER -> fire the query startSearch(); } else if(keyCode == KeyCodes.KEY_ESCAPE) { ((SuggestBox.DefaultSuggestionDisplay) searchBox.getSuggestionDisplay()).hideSuggestions(); } else if(keyCode == ' ' && event.isControlKeyDown()) { // CTRL-Space: show suggestions searchBox.showSuggestionList(); } if(((SuggestBox.DefaultSuggestionDisplay) searchBox.getSuggestionDisplay()).isSuggestionListShowing()) { // gobble up navigation keys if(keyCode == KeyCodes.KEY_UP || keyCode == KeyCodes.KEY_DOWN || keyCode == KeyCodes.KEY_ENTER) { event.stopPropagation(); event.preventDefault(); } } } }); searchBox.addKeyDownHandler(new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { int keyCode = event.getNativeKeyCode(); if(((SuggestBox.DefaultSuggestionDisplay) searchBox.getSuggestionDisplay()).isSuggestionListShowing()) { // gobble up navigation keys if(keyCode == KeyCodes.KEY_UP || keyCode == KeyCodes.KEY_DOWN || keyCode == KeyCodes.KEY_ENTER) { event.stopPropagation(); event.preventDefault(); } } } }); searchBox.addKeyPressHandler(new KeyPressHandler() { @Override public void onKeyPress(KeyPressEvent event) { int keyCode = event.getNativeEvent().getKeyCode(); if(((SuggestBox.DefaultSuggestionDisplay) searchBox.getSuggestionDisplay()).isSuggestionListShowing()) { // gobble up navigation keys if(keyCode == KeyCodes.KEY_UP || keyCode == KeyCodes.KEY_DOWN || keyCode == KeyCodes.KEY_ENTER) { event.stopPropagation(); event.preventDefault(); } } } }); searchButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { startSearch(); } }); History.addValueChangeHandler(new ValueChangeHandler<String>() { @Override public void onValueChange(ValueChangeEvent<String> event) { String historyToken = event.getValue(); logger.info("History token " + historyToken); if(historyToken != null && historyToken.length() > 0) { String newQueryId = null; String newQueryString = null; int newFirstDoc = 0; String[] elems = historyToken.split("\\&"); for(String elem : elems) { String[] keyVal = elem.split("=", 2); String key = keyVal[0].trim(); String value = keyVal[1].trim(); if(key.equalsIgnoreCase("queryId")) { newQueryId = URL.decodeQueryString(value); } else if(key.equalsIgnoreCase("queryString")) { newQueryString = URL.decodeQueryString(value); } else if(key.equalsIgnoreCase("firstDoc")) { try{ newFirstDoc =Integer.parseInt(value); } catch (NumberFormatException nfe) { // ignore, and start results from zero } } } // now update the display accordingly if(newQueryId != null && newQueryId.length() > 0) { queryId = newQueryId; queryString = newQueryString; if(!searchBox.getText().trim().equalsIgnoreCase( newQueryString.trim())){ searchBox.setText(newQueryString); } resultsUpdater.setFirstDocument(newFirstDoc); resultsUpdater.schedule(10); } } } }); // now read the current history String historyToken = History.getToken(); if(historyToken != null && historyToken.length() > 0) { History.fireCurrentHistoryState(); } } protected void updateResultsDisplay (List<DocumentData> documentsData) { searchResultsPanel.clear(); if(documentsData == null || documentsData.isEmpty()) { for(int i = 0; i < 20; i++) searchResultsPanel.add(new HTML(" ")); } else { for(DocumentData docData : documentsData) { searchResultsPanel.add(buildDocumentDisplay(docData)); } } } protected void startSearch() { // clean up old state if(queryId != null) { // release old query gwtRpcService.releaseQuery(queryId, new AsyncCallback<Void>() { @Override public void onSuccess(Void result) {} @Override public void onFailure(Throwable caught) {} }); } // reset internal data initLocalData(); // clear the results pages list pageLinksPanel.clear(); // post the new query postQuery(searchBox.getText()); } protected void postQuery(final String newQueryString) { feedbackLabel.setText("Working..."); updateResultsDisplay(null); gwtRpcService.search(getIndexId(), newQueryString, new AsyncCallback<String>() { @Override public void onFailure(Throwable caught) { feedbackLabel.setText(caught.getLocalizedMessage()); } @Override public void onSuccess(String newQueryId) { History.newItem(createHistoryToken(newQueryId, newQueryString, firstDocumentOnPage)); } }); } protected String createHistoryToken(String queryId, String queryString, int firstDocument) { return "queryId=" + URL.encodeQueryString(queryId) + "&queryString=" + URL.encodeQueryString(queryString) + "&firstDoc=" + firstDocument; } /** * Updates the results display (including the feedback label) * @param resultsData */ protected void updatePage(ResultsData resultsData) { int resTotal = resultsData.getResultsTotal(); int resPartial = resultsData.getResultsPartial(); StringBuilder textBuilder = new StringBuilder(); if(resTotal == 0) { // no results textBuilder.append("No results, try rephrasing the query."); } else { textBuilder.append("Documents "); textBuilder.append(firstDocumentOnPage + 1); textBuilder.append(" to "); if(firstDocumentOnPage + maxDocumentsOnPage < resPartial) { textBuilder.append(firstDocumentOnPage + maxDocumentsOnPage); } else { textBuilder.append(resPartial - firstDocumentOnPage); } textBuilder.append(" of "); if(resTotal >= 0) { // all results obtained textBuilder.append(resultsData.getResultsTotal()); } else { // more to come textBuilder.append("at least "); textBuilder.append(resPartial); } textBuilder.append(":"); } feedbackLabel.setText(textBuilder.toString()); // now update the documents display if(resultsData.getDocuments() != null){ updateResultsDisplay(resultsData.getDocuments()); // page links pageLinksPanel.clear(); int currentPage = firstDocumentOnPage / maxDocumentsOnPage; int firstPage = Math.max(0, currentPage - (maxPages / 2)); int maxPage = resultsData.getResultsPartial() / maxDocumentsOnPage; if(resultsData.getResultsPartial() % maxDocumentsOnPage > 0) maxPage++; maxPage = Math.min(maxPage, firstPage + maxPages); for(int pageNo = firstPage; pageNo < maxPage; pageNo++) { Widget pageLink; if(pageNo != currentPage) { pageLink = new InlineHyperlink("" + (pageNo + 1), createHistoryToken(queryId, queryString, pageNo * maxDocumentsOnPage)); } else { pageLink = new InlineLabel("" + (pageNo + 1)); } pageLink.addStyleName("pageLink"); pageLinksPanel.add(pageLink); } } } private HTMLPanel buildDocumentDisplay(DocumentData docData) { HTMLPanel documentDisplay = new HTMLPanel(""); documentDisplay.setStyleName("hit"); String documentUri = docData.documentUri; String documentTitle = docData.documentTitle; if(documentTitle == null || documentTitle.trim().length() == 0) { // we got no title to display: use the URI file String [] pathElems = documentUri.split("/"); documentTitle = pathElems[pathElems.length -1]; } String documentTitleText = "<span title=\"" + documentUri + "\" class=\"document-title\">" + docData.documentTitle + "</span>"; FlowPanel docLinkPanel = new FlowPanel(); // docLinkPanel.setStyleName("document-title"); if(uriIsLink) { // generate two links: original doc and cached docLinkPanel.add(new Anchor(documentTitleText, true, documentUri)); docLinkPanel.add(new InlineLabel(" (")); docLinkPanel.add(new Anchor("cached", false, "document?documentRank=" + docData.documentRank + "&queryId=" + queryId)); docLinkPanel.add(new InlineLabel(")")); } else { // generate one link: cached, with document name as text docLinkPanel.add(new Anchor(documentTitle, true, "document?documentRank=" + docData.documentRank + "&queryId=" + queryId)); } documentDisplay.add(docLinkPanel); if(docData.snippets != null) { StringBuilder snippetsText = new StringBuilder("<div class=\"snippets\">"); // each row is left context, snippet, right context for(String[] snippet : docData.snippets) { snippetsText.append("<span class=\"snippet\">"); snippetsText.append(snippet[0]); snippetsText.append("<span class=\"snippet-text\">"); String snipText = snippet[1]; int snipLen = snipText.length(); if(snipLen > maxSnippetLength) { int toRemove = snipLen - maxSnippetLength; snipText = snipText.substring(0, (snipLen - toRemove) / 2) + " ... " + snipText.substring((snipLen + toRemove) / 2); } snippetsText.append(snipText); //close snippet-text span snippetsText.append("</span>"); snippetsText.append(snippet[2]); //close snippet span snippetsText.append("</span>"); } documentDisplay.add(new HTML(snippetsText.toString())); } return documentDisplay; } }